package com.minivision.fdiot.rest;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;

import javax.validation.Valid;

import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.minivision.fdiot.entity.DeviceFaceSet;
import com.minivision.fdiot.entity.FaceSet;
import com.minivision.fdiot.mqtt.MqttConnectionManager;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.util.CollectionUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.google.common.base.Joiner;
import com.minivision.fdiot.annotation.Log;
import com.minivision.fdiot.annotation.NoAuth;
import com.minivision.fdiot.common.Action;
import com.minivision.fdiot.common.PageResult;
import com.minivision.fdiot.common.RestResult;
import com.minivision.fdiot.domain.AppData;
import com.minivision.fdiot.domain.DeviceInfo;
import com.minivision.fdiot.domain.FaceData;
import com.minivision.fdiot.domain.FaceSetInfo;
import com.minivision.fdiot.entity.Device;
import com.minivision.fdiot.entity.MqttLog;
import com.minivision.fdiot.exception.ErrorType;
import com.minivision.fdiot.exception.FacePlatException;
import com.minivision.fdiot.exception.ServiceException;
import com.minivision.fdiot.mqtt.handler.CommonSender;
import com.minivision.fdiot.mqtt.message.UpgradeFile;
import com.minivision.fdiot.rest.param.device.AddFace2DeviceParam;
import com.minivision.fdiot.rest.param.device.CreateDeviceParam;
import com.minivision.fdiot.rest.param.device.GetActivationCodeParam;
import com.minivision.fdiot.rest.param.device.Push2DeviceParam;
import com.minivision.fdiot.rest.param.device.PushFile2DeviceParam;
import com.minivision.fdiot.rest.param.device.QueryDeviceParam;
import com.minivision.fdiot.rest.param.device.Sync2DeviceParam;
import com.minivision.fdiot.rest.param.device.UpdateDeviceParam;
import com.minivision.fdiot.service.DeviceService;
import com.minivision.fdiot.service.FaceCommonService;
import com.minivision.fdiot.util.FeatureUtils;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping(value = "api/v1/device", method = RequestMethod.POST)
@Api(tags = "DeviceApi", value = "IoT Platform Device Apis")
@Slf4j
public class DeviceApi {
  
  @Autowired
  private FaceCommonService commonService;

  @Autowired
  private DeviceService deviceService;
  
  @Autowired
  private CommonSender sender;
  
  @Autowired
  @Qualifier("mqttSenderWorker")
  private ExecutorService mqttSenderWorker;

  @Autowired
  private MqttConnectionManager connectionManager;
  
  @RequestMapping(value = "forward2Device")
  @ApiOperation(value = "向设备转发消息", notes = "向设备转发消息", hidden = true)
  @NoAuth
  public RestResult<Boolean> forward2Device(@Valid @RequestBody MqttLog param, BindingResult errResult) throws Exception {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    
    return new RestResult<>(sender.forward(param));
  }

  @RequestMapping(value = "disConnectDevice")
  @ApiOperation(value = "中断设备连接", notes = "中断设备连接", hidden = true)
  @NoAuth
  public void disConnectDevice(Device device){
    connectionManager.closeLocalConnection(device.getSn());
  }
  
  @RequestMapping(value = "addface2Device")
  @ApiOperation(value = "向设备推送人脸", notes = "向设备推送人脸")
  public RestResult<String> addface2Device(@Valid @ModelAttribute AddFace2DeviceParam param, BindingResult errResult) throws Exception {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    
    List<Device> devices;
    List<String> deviceSns = null;
    if (StringUtils.hasText(param.getDevices())) {
      deviceSns = Arrays.stream(param.getDevices().split(",")).collect(Collectors.toList());
      devices = deviceService.findDevices(deviceSns);
    } else {
      devices = deviceService.findByAppKey(param.getAppKey());
    }
    
    if (!CollectionUtils.isEmpty(devices)) {
      List<FaceData> faceDatas = new ArrayList<>();
      String facesetToken = param.getFacesetToken();
      String faceTokens = param.getFaceTokens();
      Arrays.stream(faceTokens.split(",")).forEach((faceToken) -> {
        float[] features = null;
        try {
          features = commonService.findOneFace(faceToken).getFeature();
        } catch (FacePlatException e) {
          log.error("get face feature exception", e);
        }
        if (features != null && features.length > 0) {
          FaceData faceData = new FaceData();
          faceData.setFacesetToken(facesetToken);
          faceData.setToken(faceToken);
          faceData.setFeature(FeatureUtils.encode(features));
          faceDatas.add(faceData);
        }
      });
      
      mqttSenderWorker.execute(() -> {
        devices.stream().forEach((device) -> {
          try {
            sender.refreshFace(Action.ADD, device.getSn(), device.getModel(), faceDatas);
          } catch (Exception e) {
            log.error("refreshFace to device[{}] failed", device.getSn());
            log.error("MQTT Exception", e);
          }
        });
      });
    }
    
    return new RestResult<>("");
  }
  
  @RequestMapping(value = "push2Device")
  @ApiOperation(value = "向设备推送数据", notes = "向设备推送数据")
  public RestResult<String> push2Device(@Valid @ModelAttribute Push2DeviceParam param, BindingResult errResult) throws Exception {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    
    List<Device> devices;
    List<String> deviceSns = null;
    if (StringUtils.hasText(param.getDevices())) {
      deviceSns = Arrays.stream(param.getDevices().split(",")).collect(Collectors.toList());
      devices = deviceService.findDevices(deviceSns);
    } else {
      devices = deviceService.findByAppKey(param.getAppKey());
    }
    
    if (!CollectionUtils.isEmpty(devices)) {
      AppData data = new AppData();
      BeanUtils.copyProperties(param, data);
      JSONObject object = JSONObject.parseObject(param.getData());
      data.setData(object);
      mqttSenderWorker.execute(() -> {
        devices.stream().forEach((device) -> {
          try {
            sender.sendRawMessage(device.getSn(), device.getModel(), data);
          } catch (Exception e) {
            log.error("sendRawMessage to device[{}] failed", device.getSn());
            log.error("MQTT Exception", e);
          }
        });
      });
    }
    
    return new RestResult<>("");
  }
  
  @RequestMapping(value = "pushFile2Device")
  @ApiOperation(value = "向设备推送文件", notes = "向设备推送文件")
  @ApiImplicitParams({
    @ApiImplicitParam(name = "file", paramType = "form", dataType="file")
  })
  public RestResult<String> pushFile2Device(@Valid @ModelAttribute PushFile2DeviceParam param, BindingResult errResult) throws Exception {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    
    List<Device> devices;
    List<String> deviceSns = null;
    if (StringUtils.hasText(param.getDevices())) {
      deviceSns = Arrays.stream(param.getDevices().split(",")).collect(Collectors.toList());
      devices = deviceService.findDevices(deviceSns);
    } else {
      devices = deviceService.findByAppKey(param.getAppKey());
    }
    
    if (!CollectionUtils.isEmpty(devices)) {
      UpgradeFile fileData = new UpgradeFile();
      fileData.setFunc(param.getFunc());
      fileData.setFile(param.getFile().getBytes());
      mqttSenderWorker.execute(() -> {
        devices.stream().forEach((device) -> {
          try {
            sender.sendUpgradeFile(device.getSn(), device.getModel(), fileData);
          } catch (Exception e) {
            log.error("sendUpgradeFile to device[{}] failed", device.getSn());
            log.error("MQTT Exception", e);
          }
        });
      });
    }
    
    return new RestResult<>("");
  }
  
  @RequestMapping(value = "sync2Device")
  @ApiOperation(value = "向设备全量同步人脸库", notes = "向设备全量同步人脸库，请求Body示例："
      + "{\"facesets\":[{\"facesetToken\":\"\",\"extras\":{}},...],\"devices\":[{\"sn\":\"\",\"model\":\"\"},...]}")
  public RestResult<String> sync2Device(@Valid @RequestBody Sync2DeviceParam param, BindingResult errResult) throws Exception {
    log.info("sync2Device param:{}", param);
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    
    List<FaceSetInfo> facesets = param.getFacesets();
    List<DeviceInfo> devices = param.getDevices();
    List<String> tokens = new ArrayList<>();
    Map<String, Map<String, Object>> facesetExtraInfo = new HashMap<>();
    
    if (!CollectionUtils.isEmpty(facesets)) {
      facesets.stream().forEach((item) -> {
        if (StringUtils.hasText(item.getFacesetToken())) {
          tokens.add(item.getFacesetToken());
          facesetExtraInfo.put(item.getFacesetToken(), item.getExtras());
        }
      });
    }
    
    String facesetTokens = Joiner.on(",").join(tokens);
    
    if (!CollectionUtils.isEmpty(devices)) {
      devices.stream().forEach((device) -> {
        UpdateDeviceParam updateDeviceParam = new UpdateDeviceParam();
        updateDeviceParam.setSn(device.getSn());
        Map<String,DeviceFaceSet> facesetTokenMap = new HashMap<>();
        for(String facesetToken : facesetTokens.split("\\,")){
          try {
            FaceSet faceSet = commonService.findOneFaceset(facesetToken);
            DeviceFaceSet deviceFaceSet = new DeviceFaceSet();
            deviceFaceSet.setSn(device.getSn());
            deviceFaceSet.setFaceSetToken(facesetToken);
            deviceFaceSet.setPriority(faceSet.getPriority());
            facesetTokenMap.put(facesetToken,deviceFaceSet);
          } catch (FacePlatException e) {
            log.error(e.getMessage());
          }

        }
        updateDeviceParam.setFacesetTokenMap(facesetTokenMap);
        updateDeviceParam.setFacesetTokens(facesetTokens);
        deviceService.updateDevice(updateDeviceParam);
        
        mqttSenderWorker.execute(() -> {
          tokens.stream().forEach((facesetToken) -> {
            try {
              sender.sync2Device(device.getSn(), device.getModel(), facesetToken, facesetExtraInfo.get(facesetToken));
            } catch (Exception e) {
              log.error("sync2Device failed, device:" + device.getSn() + ", facesetToken:" + facesetToken, e);
            }
          }); 
        });
      });
    }
    
    return new RestResult<>("");
  }
  
  @RequestMapping(value = "createDevice")
  @ApiOperation(value = "新增设备", notes = "新增设备")
  @Log(module = "DeviceApi", operation = "新增设备")
  public RestResult<Device> createDevice(@Valid @ModelAttribute CreateDeviceParam param, BindingResult errResult) {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    
    Device created = null;
    try {
      created = deviceService.createDevice(param);
    } catch (Exception e) {
      log.error("save device failed", e);
      throw new ServiceException((e instanceof DataIntegrityViolationException ? "设备已存在" : "新增设备失败"));
    }
    
    return new RestResult<>(created);
  }
  
  @RequestMapping(value = "updateDevice")
  @ApiOperation(value = "修改设备信息", notes = "修改设备信息")
  @Log(module = "DeviceApi", operation = "修改设备信息")
  public RestResult<Device> updateDevice(@Valid @ModelAttribute UpdateDeviceParam param, BindingResult errResult) {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    Device updated = null;
    try {
      if(!StringUtils.isEmpty(param.getFacesetTokens())){
        Map<String,DeviceFaceSet> faceSetMap = new Gson().fromJson(param.getFacesetTokens(), new TypeToken<Map<String,DeviceFaceSet>>(){}.getType());
        param.setFacesetTokenMap(faceSetMap);
        param.setFacesetTokens(Joiner.on(",").join(faceSetMap.keySet()));
      }
      updated = deviceService.updateDevice(param);
    } catch (Exception e) {
      log.error("update device failed", e);
      throw new ServiceException("修改设备信息失败");
    }
    
    return new RestResult<>(updated);
  }
  
  @RequestMapping(value = "deleteDevice")
  @ApiOperation(value = "删除设备", notes = "删除设备")
  @ApiImplicitParams(value = {
      @ApiImplicitParam(name = "id", value = "设备ID", paramType = "query", dataType = "long"),
      @ApiImplicitParam(name = "sn", value = "设备编号", paramType = "query", dataType = "String"),
      @ApiImplicitParam(name = "appKey", value = "应用标识", paramType = "query", dataType = "String", required = true),
      @ApiImplicitParam(name = "timestamp", value = "时间戳", paramType = "query", dataType = "String", required = true)
  })
  @Log(module = "DeviceApi", operation = "删除设备")
  public RestResult<String> deleteDevice(@RequestParam(value = "id", required = false) Long id, @RequestParam(value = "sn", required = false) String sn) {
    Device existed = null;
    try {
      if (id != null && id > 0) {
        existed = deviceService.findById(id);
      } else if (StringUtils.hasText(sn)) {
        existed = deviceService.findDevice(sn);
      } else {
        throw new IllegalArgumentException("param error");
      }
      if (existed == null) {
        return new RestResult<>(ErrorType.getDesc(ErrorType.ENTITY_NOT_EXIST), ErrorType.ENTITY_NOT_EXIST);
      }
      deviceService.deleteDevice(existed.getId());
    } catch (Exception e) {
      log.error("delete device failed", e);
      throw new ServiceException("删除设备失败");
    }
    
    return new RestResult<>("");
  }
  
  @RequestMapping(value = "deleteBatch")
  @ApiOperation(value = "批量删除设备", notes = "批量删除设备")
  @ApiImplicitParams(value = {
      @ApiImplicitParam(name = "deviceIds", value = "设备ID，多个值以英文逗号分隔", paramType = "query", dataType = "String"),
      @ApiImplicitParam(name = "deviceSns", value = "设备编号，多个值以英文逗号分隔", paramType = "query", dataType = "String"),
      @ApiImplicitParam(name = "appKey", value = "应用标识", paramType = "query", dataType = "String", required = true),
      @ApiImplicitParam(name = "timestamp", value = "时间戳", paramType = "query", dataType = "String", required = true)
  })
  @Log(module = "DeviceApi", operation = "批量删除设备")
  public RestResult<String> deleteBatch(@RequestParam(value = "deviceIds", required = false) String deviceIds, @RequestParam(value = "deviceSns", required = false) String deviceSns) {
    try {
      if (StringUtils.hasText(deviceIds)) {
        deviceService.deleteBatch(StringUtils.commaDelimitedListToSet(deviceIds).stream().map(s -> NumberUtils.parseNumber(s, Long.class)).collect(Collectors.toList()));
      } else if (StringUtils.hasText(deviceSns)) {
        deviceService.deleteBatchBySn(StringUtils.commaDelimitedListToSet(deviceSns));
      } else {
        throw new IllegalArgumentException("param error");
      }
    } catch (Exception e) {
      log.error("batch delete devices failed", e);
      throw new ServiceException("批量删除设备失败");
    }
    
    return new RestResult<>("");
  }
  
  @RequestMapping(value = "activate")
  @ApiOperation(value = "激活设备", notes = "激活设备", hidden = true)
  @ApiImplicitParams(value = {
      @ApiImplicitParam(name = "deviceSn", value = "设备编号", paramType = "query", dataType = "String", required = true)
  })
  public RestResult<String> activate(@RequestParam("deviceSn") String deviceSn) {
    if (!StringUtils.hasText(deviceSn)) {
      throw new ServiceException("请指定要激活的设备编号！");
    }
    
    try {
      deviceService.activateDevice(deviceSn);
    } catch (Exception e) {
      log.error("activate device failed", e);
      throw new ServiceException("激活设备失败");
    }
    
    return new RestResult<>("");
  }
  
  @RequestMapping(value = "deviceList")
  @ApiOperation(value = "获取设备列表", notes = "获取设备列表")
  public RestResult<PageResult<Device>> deviceList(@Valid @ModelAttribute QueryDeviceParam param, BindingResult errResult) {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }

    PageResult<Device> result = new PageResult<>();
    try {
      Page<Device> page = deviceService.findByPage(param);
      if (page != null && page.hasContent()) {
        result.setPages(page.getTotalPages());
        result.setTotal(page.getTotalElements());
        result.setRows(page.getContent());
      }
    } catch (Exception e) {
      log.error("get device list failed", e);
      throw new ServiceException("获取设备列表失败");
    }
    
    return new RestResult<>(result);
  }
  
  @RequestMapping(value = "deviceDetail")
  @ApiOperation(value = "查询设备详情", notes = "查询设备详情")
  @ApiImplicitParams(value = {
      @ApiImplicitParam(name = "deviceSns", value = "设备编号，多个值以英文逗号分隔", paramType = "query", dataType = "String"),
      @ApiImplicitParam(name = "appKey", value = "应用标识", paramType = "query", dataType = "String", required = true),
      @ApiImplicitParam(name = "timestamp", value = "时间戳", paramType = "query", dataType = "String", required = true)
  })
  public RestResult<List<Device>> deviceDetail(@RequestParam String deviceSns) {
    if (!StringUtils.hasText(deviceSns)) {
      throw new IllegalArgumentException("param error");
    }
    List<Device> result = null;
    try {
      result = deviceService.findDevices(Arrays.stream(deviceSns.split(",")).collect(Collectors.toList()));
    } catch (Exception e) {
      log.error("get device detail failed", e);
      throw new ServiceException("查询设备详情失败");
    }
    
    return new RestResult<>(result == null ? new ArrayList<>() : result);
  }
  
  @RequestMapping(value = "activationCode")
  @ApiOperation(value = "获取设备激活码", notes = "获取设备激活码，激活码为16位字符，显示时用分隔符以4位分隔显示")
  public RestResult<String> activationCode(@Valid @ModelAttribute GetActivationCodeParam param, BindingResult errResult) {
    if (errResult.hasErrors()) {
      List<ObjectError> errorList = errResult.getAllErrors();
      for(ObjectError error : errorList){
        log.error(error.getDefaultMessage());
      }
      throw new ServiceException(errorList.get(0).getDefaultMessage());
    }
    
    String code = "";
    try {
      code = deviceService.getActivationCode(param.getModel(), param.getSn());
    } catch (Exception e) {
      log.error("get device activation code failed", e);
      throw new ServiceException("获取设备激活码失败");
    }
    
    return new RestResult<>(code);
  }
  
}
